AWS Solutions Constructを使用してOACとCloudFront ディストリビューションを簡単に実装できないか試してみた

AWS Solutions Constructを使用してOACとCloudFront ディストリビューションを簡単に実装できないか試してみた

Clock Icon2024.08.21

はじめに

S3 オリジンへのアクセスをCloudFront ディストリビューションのみに制限する際に、OAI(Origin access identity)が使用されてきましたがセキュリティの観点などから現在はOAC(Origin access control)の使用が推奨されています。
Amazon CloudFront オリジンアクセスコントロール(OAC)のご紹介

CDKで構築する場合、L2コンストラクトがまだ提供されていないため、L1コンストラクトを使用する必要がありますが記述量が多くなってしまいます。

そこで、もっと記述量を減らし簡単に実装できる方法を探してみたところ、AWS Solutions Constructsのaws-cloudfront-s3を使用する方法がありましたので、このブログで紹介したいと思います。npmの公式ページには、以下のようにOACを使うことが前提のモジュールであることが記載されています。

このAWS Solutions Constructは、Origin Access Control(OAC)を介してAWS S3バケットからオブジェクトを提供するAmazon CloudFront Distributionをプロビジョニングします。

この方法を使うと、かなり記述量少なくOACを使用したオリジン設定が簡単に実装でき、OACについては特に記述しなくても最低限のバケットポリシーと共にデフォルトの設定で作成してくれます。

結論

すごく簡単に実装できました!!

AWS Solutions Constructsとは

AWS Solutions ConstructsはAWS CDKのオープンソース拡張機能で、公式ドキュメントには以下のように説明されています。

AWS Solutions Constructs ライブラリは、AWS クラウド開発キット (AWS CDK) のオープンソース拡張機能であり、コードでソリューションをすばやく定義して予測可能で繰り返し可能なインフラストラクチャを作成するための、マルチサービスの Well-Architected パターンを提供します。AWS Solutions Constructs の目標は、アーキテクチャのパターンベースの定義を使用して、開発者があらゆる規模のソリューションを構築するエクスペリエンスを加速することです。

よく使われるアーキテクチャのパターンを再利用して構築できるようにモジュール化されたものですね。

使用したモジュールのバージョン

  • aws-cdk: 2.131.0
  • aws-cdk-lib: 2.150.0
  • node js: 20.16.0
  • typescript: 5.4.5
  • @aws-solutions-constructs/aws-cloudfront-s3: 2.65.0

CDKのコード

CDKの全体のコードは以下の通りとなります。

import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3";
import { aws_s3, aws_ssm, aws_cloudfront, aws_iam } from "aws-cdk-lib";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const accountId = cdk.Stack.of(this).account;
    const region = cdk.Stack.of(this).region;

    const s3Bucket = new aws_s3.Bucket(this, `s3Bucket`, {
      bucketName: `s3-bucket-${region}-${accountId}`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
      existingBucketObj: s3Bucket,
      cloudFrontDistributionProps: {
        defaultBehavior: {
          allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
          viewerProtocolPolicy:
            aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        },
      },
    });

    // s3:ListBucketをバケットポリシーに追加
    const bucketPolicy = new aws_iam.PolicyStatement({
      effect: aws_iam.Effect.ALLOW,
      principals: [new aws_iam.ServicePrincipal("cloudfront.amazonaws.com")],
      actions: ["s3:ListBucket"],
      resources: [s3Bucket.bucketArn],
      conditions: {
        StringEquals: {
          "AWS:SourceArn": `arn:aws:cloudfront::${
            cdk.Stack.of(this).account
          }:distribution/${
            cloudFrontToS3.cloudFrontWebDistribution.distributionId
          }`,
        },
      },
    });
    s3Bucket.addToResourcePolicy(bucketPolicy);
  }
}

コードの解説

肝心なCloudFront ディストリビューションとOACの作成は以下のコードだけで構築できます。かなり記述が少なく済みますね!

    const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
      existingBucketObj: s3Bucket,
      cloudFrontDistributionProps: {
        defaultBehavior: {
          allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
          viewerProtocolPolicy:
            aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        },
      },
    });

CloudFront ディストリビューションとOAC

  • existingBucketObj
    • オリジンにしたいS3バケットを指定します。
  • defaultBehavior
    • aws-cdk-libaws_cloudfrontを使って設定する時のdefaultBehaviorと同じようにビヘイビアを設定する箇所です。
ハマりポイント

aws-cdk-libaws_cloudfront_originを使ってdefaultBehaviorのパラメーターとしてオリジンを設定してデプロイすると以下のエラーが発生します。

  • コード
    const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
      cloudFrontDistributionProps: {
        defaultBehavior: {
          origin: new aws_cloudfront_origins.S3Origin(s3Bucket), // <-- ここでオリジンを設定するとエラーが発生
          allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
          viewerProtocolPolicy:
            aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        },
      },
    });
  • エラー内容

Cannot use both Origin Access Control and Origin AccessIdentity on an origin

2:46:50 PM | UPDATE_FAILED | AWS::CloudFront::Distribution | cloudfront-s3...CloudFrontDistribution
Resource handler returned message: "Invalid request provided: Cannot use both Origin Access Control and Origin AccessIdentity on an origin (Service: CloudFront, Status Code: 400, Request ID: 4ba4d9c0-5ed3-44c8-a197-467b790911af)" (RequestToken: 3407237b-8bed-a84c-ba8e-dadc893b3ff9, HandlerErrorCode: InvalidRequest)

defaultBehaviorの中でオリジンを設定すると従来のOAI(Origin AccessIdentity)を使って構築しようとするようです。このため、規定でOACを使ってオリジン設定する動きと競合して上記のエラーが発生するようです。

バケットポリシーの設定

ブロックパブリックアクセスを有効(デフォルトでは有効)にしているオリジンのバケットでは、OACが割り当てられているCloudFront ディストリビューションからのみのアクセスをバケットポリシーで許可する必要があります。今回のモジュール(aws-cloudfront-s3)を使ってデプロイすると、以下の通り、s3:GetObjectについては自動的に追加してくれます。

バケットポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3-bucket-ap-northeast-1-123456789012/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": <ディストリビューションのARN>
                }
            }
        }
    ]
}

しかしながら、これだけではバケットに対象のオブジェクトが存在しない場合に返却されるHTTPステータスコードが403 Forbiddenとなってしまいます。このため、バケットポリシーにs3:ListBucketを追加して、バケットに対象のオブジェクトが存在しない場合にも404 Not Foundを返すようにしてユーザビリティを向上させておくのが良いかと思います。

バケットポリシーにs3:ListBucketを追加
    const bucketPolicy = new aws_iam.PolicyStatement({
      effect: aws_iam.Effect.ALLOW,
      principals: [new aws_iam.ServicePrincipal("cloudfront.amazonaws.com")],
      actions: ["s3:ListBucket"],
      resources: [s3Bucket.bucketArn],
      conditions: {
        StringEquals: {
          "AWS:SourceArn": `arn:aws:cloudfront::${
            cdk.Stack.of(this).account
          }:distribution/${
            cloudFrontToS3.cloudFrontWebDistribution.distributionId
          }`,
        },
      },
    });
    s3Bucket.addToResourcePolicy(bucketPolicy);

デプロイ

デプロイすると以下のようにOACが作成されています。
image-ewlwidlso

CloudFrontにリクエストを送信

デプロイ後、CloudFrontにリクエストを送信してみます。

オブジェクトが存在する場合

オリジンに設定したS3に保存したJSONファイルの中身が返ってきました。

$ curl -i https://<ディストリビューションドメイン名>.cloudfront.net/sample.json

HTTP/2 200
content-type: application/json
content-length: 33
date: Wed, 21 Aug 2024 06:29:19 GMT
last-modified: Wed, 21 Aug 2024 06:28:13 GMT
x-amz-expiration: expiry-date="Sun, 25 Aug 2024 00:00:00 GMT", rule-id="MzEzMDVhOGQtZDgyMi00NWE1LWI2ZTctNzc3ZjBlNDMzYWU3"
etag: "bd73e16217411a2b3780ab4bd30c1685"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
via: 1.1 0932afdcbb622a4425fd671f0d67863a.cloudfront.net (CloudFront)
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
strict-transport-security: max-age=63072000; includeSubdomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
x-cache: Miss from cloudfront
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: lDVMqh9nWpi0HD0WQVCRjFRZJ7LUZV170Z1PFObdqCB1-CIN02z0uw==

{
  "message": "Hello, World!"
}

オブジェクトが存在しない場合

バケットポリシーにs3:ListBucketを追加したので、ステータスコード404とNoSuchKeyエラーが返ってきました。

$ curl -i https://<ディストリビューションドメイン名>.cloudfront.net/not-exist.json

HTTP/2 404
content-type: application/xml
date: Wed, 21 Aug 2024 06:32:31 GMT
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 e5907f334714433599a0e1b9c57f44d6.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: zTESYU-Moq5bN6AawBi7kZ9rstx1B_xGx7vnlNv3UKSk6Ry6m4iFJg==

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>not-exist.json</Key><RequestId>4F7Q5XZZNCFA36AA</RequestId><HostId>MZi5IiPW906nsNGFNAGN6/V/0e6ztPxS0YhKHuUax74m+rHxEL/ywzsZSEPhWKbQkJEhvJXGd3E=</HostId></Error>%

以上。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.